﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Rosetta.Default
{
    public sealed class Container : IContainer
    {
        // TODO: Dispose all created templates
        // TODO: Sort data

        #region Fields

        private readonly Dictionary<Type, IAddIn> addIns = new Dictionary<Type, IAddIn>();

        private readonly HashSet<Type> argumentTypes = new HashSet<Type>();

        private readonly HashSet<string> categories = new HashSet<string>();

        private readonly HashSet<Type> inputReturnTypes = new HashSet<Type>();

        private readonly HashSet<Assembly> loadedAssemblies = new HashSet<Assembly>();

        private readonly HashSet<Type> returnTypes = new HashSet<Type>();

        #endregion

        #region Public Properties

        [NotNull]
        public Type[] ArgumentTypes
        {
            get
            {
                return argumentTypes.ToArray();
            }
        }

        [NotNull]
        public string[] Categories
        {
            get
            {
                return categories.ToArray();
            }
        }

        [NotNull]
        public Type[] InputReturnTypes
        {
            get
            {
                return inputReturnTypes.ToArray();
            }
        }

        [NotNull]
        public Type[] ReturnTypes
        {
            get
            {
                return returnTypes.ToArray();
            }
        }

        #endregion

        #region Public Methods

        public void Dispose()
        {
            argumentTypes.Clear();
            categories.Clear();
            loadedAssemblies.Clear();
            returnTypes.Clear();

            addIns.Clear();
        }

        [CanBeNull]
        public IAddIn GetAddIn([NotNull] string addInName)
        {
            if (string.IsNullOrEmpty(addInName))
            {
                throw new ArgumentNullException("addInName");
            }

            var addInType = Type.GetType(addInName);
            if (addInType == null)
            {
                throw new ArgumentException(string.Format("Add-in type '{0}' does not exists.", addInName), "addInName");
            }

            IAddIn result;
            if (!addIns.TryGetValue(addInType, out result))
            {
                LoadAssembly(addInType.Assembly);
                addIns.TryGetValue(addInType, out result);
            }

            return result;
        }

        [NotNull]
        public IEnumerable<IAccepts> GetAddInsByArgumentType([NotNull] Type argumentType)
        {
            if (argumentType == null)
            {
                throw new ArgumentNullException("argumentType");
            }

            return addIns.Values.OfType<IAccepts>().Where(a => a.GetMethodsByArgumentType(argumentType).Length > 0);
        }

        [NotNull]
        public IEnumerable<IAddIn> GetAddInsByCategory([NotNull] string category)
        {
            if (string.IsNullOrEmpty(category))
            {
                throw new ArgumentNullException("category");
            }

            return addIns.Values.Where(a => a.Category == category);
        }

        [NotNull]
        public IEnumerable<IReturns> GetAddInsByReturnType([NotNull] Type returnType)
        {
            if (returnType == null)
            {
                throw new ArgumentNullException("returnType");
            }

            return addIns.Values.OfType<IReturns>().Where(a => a.GetMethodsByReturnType(returnType).Length > 0);
        }

        [CanBeNull]
        public IConverter GetConverter([NotNull] string addInName)
        {
            return (IConverter)GetAddIn(addInName);
        }

        [CanBeNull]
        public IInputter GetInputter([NotNull] string addInName)
        {
            return (IInputter)GetAddIn(addInName);
        }

        [CanBeNull]
        public IOutputter GetOutputter([NotNull] string addInName)
        {
            return (IOutputter)GetAddIn(addInName);
        }

        public void LoadAssembly([NotNull] Assembly assembly)
        {
            if (assembly == null)
            {
                throw new ArgumentNullException("assembly");
            }

            if (!loadedAssemblies.Contains(assembly))
            {
                foreach (var addInType in assembly.GetTypes().Where(t => !t.IsAbstract && typeof(IAddIn).IsAssignableFrom(t)))
                {
                    try
                    {
                        var addIn = Activator.CreateInstance(addInType) as IAddIn;

                        if (addIn != null)
                        {
                            addIns.Add(addInType, addIn);
                            categories.Add(addIn.Category);

                            var accepts = addIn as IAccepts;
                            if (accepts != null)
                            {
                                foreach (var argumentType in accepts.ArgumentTypes)
                                {
                                    argumentTypes.Add(argumentType);
                                }
                            }

                            var returns = addIn as IReturns;
                            if (returns != null)
                            {
                                bool inputter = addIn is IInputter;
                                foreach (var returnType in returns.ReturnTypes)
                                {
                                    returnTypes.Add(returnType);
                                    if (inputter)
                                    {
                                        inputReturnTypes.Add(returnType);
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new ArgumentException(string.Format("Can't load add-in '{0}' in '{1}'.", addInType.Name, assembly.FullName), "assembly", ex);
                    }
                }

                loadedAssemblies.Add(assembly);
            }
        }

        #endregion
    }
}